pytestの使い方と便利な機能 | 您所在的位置:网站首页 › pytest conftestpy › pytestの使い方と便利な機能 |
pytestはPythonのテストフレームワークの一つ。 unittestなど他のフレームワークと比較して、テストに失敗した原因が分かりやすい。 この記事ではpytestの使い方に関して、公式のドキュメントを参考にメモする。 インストールpipなどを使用してインストールする。 pip install pytest 基本的な使い方基本的にはassertで望む結果を書く。 ここではtest_assert1.pyというテスト用のファイルを作成する。test_で始まる関数がテスト対象となる。ディスカバリーのルールに関してはこの記事の最後に明記する。 # content of test_assert1.py def f(): return 3 def test_function(): assert f() == 4テストの実行はpytestコマンドを利用する。 上記は、f()の結果が4でないため、Failureとなる。 $ pytest test_assert1.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item test_assert1.py F [100%] ================================= FAILURES ================================= ______________________________ test_function _______________________________ def test_function(): > assert f() == 4 E assert 3 == 4 E + where 3 = f() test_assert1.py:5: AssertionError ========================= 1 failed in 0.12 seconds ========================= 期待される例外の処理例外が発生することが期待される場合にはpytest.raisesを利用する。 以下の通りすると、ZeroDivisionErrorが発生すればテスト成功、発生しなければテスト失敗となる。 import pytest def test_zero_division(): with pytest.raises(ZeroDivisionError): 1 / 0実際の例外にアクセスしたい場合は以下の通りに行う。 import pytest def test_recursion_depth(): with pytest.raises(RuntimeError) as excinfo: def f(): f() f() assert 'maximum recursion' in str(excinfo.value) fixturesfixturesはunittestのsetup/teardownのような関数を劇的に改善する関数である。 関数の引数としてのFixturesテスト関数は引数として名前を指定することでfixtureオブジェクトを受け取ることができる。それぞれの引数名に対して、その名前を持つfixture関数がfixtureオブジェクトを提供する。fixture関数は@pytest.fixtureとともに作成することで登録される。 以下がシンプルな例である。 # content of ./test_smtpsimple.py import pytest @pytest.fixture def smtp_connection(): import smtplib return smtplib.SMTP("smtp.gmail.com", 587, timeout=5) def test_ehlo(smtp_connection): response, msg = smtp_connection.ehlo() assert response == 250 assert 0 # for demo purposes conftest.pyを利用したfixture関数の共有fixture関数を複数のテストファイルから使用したいならば、それをconftest.pyファイルに移動するとよい。テストファイルの中で明示的にconftest.pyをimportする必要はなく、自動的にpytestによって発見される。 fixture関数のディスカバリーはテスト関数から始まり、テストモジュール、conftest.py、最後に組み込み、サードパーティープラグインの順に行われる。 テストデータの共有もしテストデータをファイルから利用したい場合は、fixtureの中でそれらのデータを読み込むのは良い方法である。これはpytestの自動キャッシュ機構を利用することができる。 他の良いアプローチはtestsフォルダーの中にデータファイルを追加することである。pytest-datadirやpytest-datafilesのようなプラグインを利用すれば、このようなテストを管理することを助ける。 テスト間・クラス間・セッション間のfixtureインスタンスの共有fixtureのscope引数にmoduleを指定すると、テストモジュールごとに1度fixturesが呼び出される。(デフォルトではテスト関数ごとに1度) # content of conftest.py import pytest import smtplib @pytest.fixture(scope="module") def smtp_connection(): return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)もし、全てのテストで共有したい場合はsessionを指定する。 @pytest.fixture(scope="session") def smtp_connection(): # the returned fixture value will be shared for # all tests needing it ... Fixtureの終了処理returnの代わりにyieldを使用すると、yield文以降の全てのコードは終了処理コードとして扱われる。 # content of conftest.py import smtplib import pytest @pytest.fixture(scope="module") def smtp_connection(): smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5) yield smtp_connection # provide the fixture value print("teardown smtp") smtp_connection.close()with文を使用してシームレスにyieldを使用することも可能である。 # content of test_yield2.py import smtplib import pytest @pytest.fixture(scope="module") def smtp_connection(): with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection: yield smtp_connection # provide the fixture valueただし、もしyieldの前に例外が発生した場合は、yield以降のコードが実行されない。 終了処理コードを実行するためのもうひとつのオプションは、finalization関数を登録するために、リクエストコンテキストのaddfinalizerメソッドを使用することである。 # content of conftest.py import smtplib import pytest @pytest.fixture(scope="module") def smtp_connection(request): smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5) def fin(): print("teardown smtp_connection") smtp_connection.close() request.addfinalizer(fin) return smtp_connection # provide the fixture valueyieldとaddfinalizerメソッドは似ているが、2つの点でaddfinalizerはyieldとは異なる。 複数のfinalizer関数を登録することが出来る。 finalizersはsetupコードが例外を挙げたかどうかによらず、いつも呼ばれる。これはfixtureによって生成された全てのリソースを適切にクローズするのに便利である。 @pytest.fixture def equipments(request): r = [] for port in ('C1', 'C3', 'C28'): equip = connect(port) request.addfinalizer(equip.disconnect) r.append(equip) return r上記の例で、もしC28が例外を伴って失敗したとしても、C1とC3は適切にクローズされる。もちろん、もしfinalize関数が登録される前に例外が発生した場合には、それが実行されることはない。 fixture関数をfixture関数から利用するfixtureはfixtureからも利用できる。これはfixtureをモジュラーデザインにすることに貢献し、多くのプロジェクトに渡ってフレームワーク固有のfixtureを再利用することを可能にする。 # content of test_appsetup.py import pytest class App(object): def __init__(self, smtp_connection): self.smtp_connection = smtp_connection @pytest.fixture(scope="module") def app(smtp_connection): return App(smtp_connection) def test_smtp_connection_exists(app): assert app.smtp_connection上記の例では、app fixtureを定義し、それは前もって定義されたsmtp_connection fixtureを受け取り、Appオブジェクトとともにインスタンス化される。 パラメータ化したfixtures@pytest.mark.parametrizeを利用すれば、テスト関数のための引数をパラメータ化することができる。 # content of test_expectation.py import pytest @pytest.mark.parametrize("test_input,expected", [ ("3+5", 8), ("2+4", 6), ("6*9", 42), ]) def test_eval(test_input, expected): assert eval(test_input) == expected上記の例では3つの(test_input, expected)というタプルを定義しており、test_eval関数はそれらを使用して3回実行される。 複数のパラメータの全ての組み合わせを試験したい場合は、parametrizeをスタックできる。 import pytest @pytest.mark.parametrize("x", [0, 1]) @pytest.mark.parametrize("y", [2, 3]) def test_foo(x, y): pass上記の例では、x=0/y=2, x=1/y=2, x=0/y=3, and x=1/y=3の組み合わせでテストされる。 一時的なファイルの利用tmpdir fixtureを利用すれば、一時的なディレクトリを利用することが出来る。 tmpdirはpy.path.localオブジェクトであり、それはos.pathなどのメソッドを提供する。以下は使用例である。 # content of test_tmpdir.py import os def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") p.write("content") assert p.read() == "content" assert len(tmpdir.listdir()) == 1 assert 0 tmpdir_factorytmpdir_factoryはsessionのスコープで動作するfixtureであり、どんなfixtureやテストからも利用できる任意の一時的なディレクトリを作るのに使用できる。 例えば、以下の例では、大きなサイズのイメージをテストごとに作成する代わりに、セッションごとに1度生成することで時間を抑えることが出来る。 # contents of conftest.py import pytest @pytest.fixture(scope="session") def image_file(tmpdir_factory): img = compute_expensive_image() fn = tmpdir_factory.mktemp("data").join("img.png") img.save(str(fn)) return fn # contents of test_image.py def test_histogram(image_file): img = load_image(image_file) # compute and test histogram monkey patching / mockテスト対象となるコードの一部を差し替えるために、monkey patchingが使用できる。 monkey patchingを利用すれば、ソースコード内で呼び出される関数やアイテム、環境変数などを設定、削除することができる。 以下はシンプルな例である。以下ではos.path.expanduserの関数がtest_mytest内で定義されるmockreturnに置き換えられる。 # content of test_module.py import os.path def getssh(): # pseudo application code return os.path.join(os.path.expanduser("~admin"), '.ssh') def test_mytest(monkeypatch): def mockreturn(path): return '/abc' monkeypatch.setattr(os.path, 'expanduser', mockreturn) x = getssh() assert x == '/abc/.ssh'次に以下の例では、request.session.Session.requestメソッドが削除され、http requestが作成されるとテストに失敗するようになることが予想される。 # content of conftest.py import pytest @pytest.fixture(autouse=True) def no_requests(monkeypatch): monkeypatch.delattr("requests.sessions.Session.request") よいアプリケーション構成 標準的なdiscoverypytestは以下のようなディスカバリーを実装している。 引数が何も指定されていない場合は、testpaths(もし設定されていれば)かカレントディレクトリからテストファイルを捜索する かわりにディレクトリ、ファイル名の組み合わせを引数に利用することも出来る norecursedirsにマッチしない限り、ディレクトリは再帰的に捜索される それぞれのディレクトリではtest_*.pyもしくは*_test.pyに一致するファイルが捜索され、それらのテストパッケージがインポートされる それぞれのファイルからテストアイテムが集められる test_というプリフィックスが付けられたクラス外の関数、もしくはメソッド Testというプリフィックスが付いたクラス内の中で、test_というプリフィックスが付いた関数やメソッド(__init__メソッドは除く) テストレイアウト/インポートルールの選択pytestは以下の2つのレイアウトをサポートする。 アプリケーションコード外のテスト アプリケーションコードの一部としてのテストここでは「アプリケーションコード外のテスト」のみを扱う。 以下のような構成にすると、インストールされたバージョンのmypkgに対して簡単にテストを実行することが可能である。 setup.py mypkg/ __init__.py app.py view.py tests/ test_app.py test_view.py ...このようなスキームを使用する場合、テストファイルは必ずユニークな名前にしなければならない。なぜならば、完全なパッケージ名を取得するためのパッケージがないため、pytestはそれらをトップレベルのモジュールとしてインポートするからである。言い換えるならば、sys.pathにtests/を追加することにより、上記の例のテストファイルは、トップレベルモジュールのtest_appとtest_viewとしてインポートされる。 もし、同じ名前のテストモジュールが必要であるならば、それらをパッケージにするために、__init__.pyをtestsフォルダーとサブフォルダーに追加するであろう。 setup.py mypkg/ ... tests/ __init__.py foo/ __init__.py test_view.py bar/ __init__.py test_view.pyこの場合、pytestがtests.foo.test_viewとtests.bar.test_viewとしてモジュールをロードして、同じ名前のモジュールを持つことが可能となる。しかし、これだとちょっとした問題を引き起こす。それは、testsディレクトリからモジュールをロードするために、pytestがsys.pathの先頭にレポジトリのルートを追加するが、これがmypkgもインポート可能となるということを引き起こすからである。 もし仮想環境においてパッケージをテストするためにtoxのようなツールを利用しているならば、これは問題となる。なぜなら、テストしたいのはインストールされたバージョンのパッケージであり、レポジトリのローカルコードではないからだ。 このようなシチュエーションではsrcレイアウトを使用することが強く推奨される。その場合、アプリケーションのルートパッケージは、レポジトリのルートのサブディレクトリの中に居座り続ける。 setup.py src/ mypkg/ __init__.py app.py view.py tests/ __init__.py foo/ __init__.py test_view.py bar/ __init__.py test_view.pyこのレイアウトの有効性に関しては以下のブログに記載されている。 https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure |
CopyRight 2018-2019 实验室设备网 版权所有 |